昨天瞭解了切片的使用方式,但光會使用而不了解切片的原理,絕對會在未來吃一些不明所以的虧,所以我們立馬來了解一下切片到底從何而來以及怎麼運作!
根據前幾天的學習,我們知道陣列是一種資料型別,而正常有型別的資料是 可以被複製 以及 可以被比較 ,但複製出來的陣列跟本來的陣列本質上是不一樣的。切片因為是以陣列為核心所衍發出來的,所以有些微的不同: 切片不能被複製 ,切片其實有點類似指標,每個切片他都有指向一個底層的隱藏陣列,而資料事實上是存在這個隱藏陣列中的,那為什麼這個陣列可以自由決定長度呢?
其實是因為切片有以下三項屬性,而我們稱這三個屬性為底層隱藏陣列的 窗格 (window) 。
切片自帶手指頭,當你呼叫一個切片,他會指向底層陣列的起始位置,這就是我們平常找到切片的原理。
舉個例子:今天我在捷運松山站,要去捷運小巨蛋站,於是我呼叫松山新店線[2],如下圖,黃色箭頭指引我松山新店線底層陣列的起始位置,這下要找到索引二(捷運小巨蛋站)絕對是一塊小蛋糕。
切片的長度就是此切片內現有的元素數量,我們一樣可以使用 len() 函式來查詢切片的長度。
切片的容量其實就是切片之所以可以伸縮自如的秘訣,也就是這個切片總共可以容納的空間,今天當你 append() 一個值到切片中時,第一時間會先確認此切片是否有可容納空間,有的話就會把值丟進這個切片身後的隱藏陣列; 沒有的話首先隱藏陣列會先找一個更大的位置安頓,先產生一個新的容量更大的陣列,然後將舊的值複製過去,再加上這次 append() 的值,當然同時切片的指標也會改指為新隱藏陣列的起始位置,若我們想知道此切片的容量,使用內建函式 cap() 即可查詢容量長度。
舉個例子:今天我和朋友A一起搭捷運,朋友B也加入了,因為還有位置朋友B就依序做下去(如下圖)
但我這個人朋友比較多,朋友C、朋友D也依序上車了,我們本來位置的容量就不夠了,所以我們就換到更長的位置依序坐下(如下圖),而切片的指標也會從原本的位置,改成指向新位置的起始位置,這就是容量的概念。
有時候我們想要控制切片的長度或容量,這時可以用內建函式 make() 來定義一個初始化的切片
<新的初始切片> = make(<切片型別>, <長度>, [<容量>])
補充:
make() 函式的長度參數為必填,而容量若省略不填的話就會預設是長度,而這個新切片裡元素的值皆為零值。
若一開始就肯定切片的容量,製作一個固定容量的切片是可以提升效能,避免隱藏陣列還要找更大位置。
範例 1:
package main
import (
"fmt"
)
func main() {
var slice1 []string // 用 var 建立切片 slice1
slice2 := make([]bool,5) // 用 make() 函式建立 bool 型別,長度為 5 的切片(沒設容量容量預設跟長度一樣為 5 )
slice3 := make([]int,5,10) // 用 make() 函式建立 int 型別,長度為 5 ,容量為 10 的切片
fmt.Println("slice1:",slice1," len:",len(slice1)," cap:",cap(slice1))
fmt.Println("slice2:",slice2," len:",len(slice2)," cap:",cap(slice2))
fmt.Println("slice3:",slice3," len:",len(slice3)," cap:",cap(slice3))
}
範例 1(執行結果):
slice1: [] len: 0 cap: 0
slice2: [false false false false false] len: 5 cap: 5
slice3: [0 0 0 0 0] len: 5 cap: 10
我們了解了每個切片實際上比較像是指標,指向背後的幕後黑手 隱藏陣列 ,且實際值也是存在隱藏陣列中後,還有很重要的兩個觀念需要瞭解清楚
正常從某切片為基礎,新建出來的切片都是 指向同一個底層陣列 ,只是這些切片各自的長度可能不盡相同,但特別注意的是,當今天你更換某切片的元素值,可能會 連同指向同一個底層陣列的其他切片元素值一同被更改
,這就是前面說到的沒了解原理而可能吃一些不明所以的虧。
雖然可能有很多切片指向同一個底層隱藏陣列,但今天當對其中某個切片新增元素值,而超過本來隱藏陣列容量時,隱藏陣列除了換更大的位置,切片也會指向新隱藏陣列的起始位置,這代表著 新增超過本來容量元素值的切片,指向的隱藏陣列已經和本來隱藏陣列不相同了
,而這個行為我們稱為 陣列置換 。
今天介紹了切片的內部運作及隱藏陣列的存在,明天會使用各種方式來複製切片,來看看用不同的複製方式與隱藏陣列的關聯,那我們明天見~